﻿#region License

// PentaBiz - Sustainable Software Development Framework Copyright (C) 2013 Zoltán Csizmazia
// 
// This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

#endregion

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Mapping;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using PentaBiz.Diagnostics;

namespace PentaBiz.Cloud.CloudDb
{
    [Serializable]
    public class PreGeneratedViews
    {
        public string EdmEntityContainerName { get; set; }
        public string StoreEntityContainerName { get; set; }
        public string HashOverMappingClosure { get; set; }
        public string HashOverAllExtentViews { get; set; }
        public int ViewCount { get; set; }


        public string Views { get; set; }
        public string ViewsEmbeddedResourceName { get; set; }
    }

    public static class DbContextPreGenerateViewExtension
    {
        public static PreGeneratedViews PreGenerateViews<T>(this T dbContext) where T : DbContext
        {
            Trace.TraceInformation("PreGenerating views");

            //define ef collections
            EdmItemCollection edmItemCollection = null;
            StoreItemCollection storeItemCollection = null;
            StorageMappingItemCollection mappingItemCollection = null;

            //get ef collections
            GetItemCollections(
                GetEdmx(dbContext),
                out edmItemCollection,
                out storeItemCollection,
                out mappingItemCollection);

            IList<EdmSchemaError> errors = null;
            //get the generated views
            Dictionary<string, string> extentViews = GetExtentViews(mappingItemCollection, out errors);


            //return the pregenerated views as string (xml document)
            return new PreGeneratedViews
                {
                    EdmEntityContainerName = edmItemCollection.GetItems<EntityContainer>().Single().Name,
                    StoreEntityContainerName = storeItemCollection.GetItems<EntityContainer>().Single().Name,
                    HashOverMappingClosure =
                        ReflectionHelper.GetMappingClosureHash(edmItemCollection.EdmVersion,
                                                               mappingItemCollection),
                    HashOverAllExtentViews =
                        ReflectionHelper.GenerateHashForAllExtentViewsContent(edmItemCollection.EdmVersion,
                                                                              extentViews),
                    ViewCount = extentViews.Count,
                    Views = CreateViews(extentViews),
                    ViewsEmbeddedResourceName =
                        string.Format("DbContextViews{0}.xml", Guid.NewGuid().ToString("N")),
                };
        }

        private static XDocument GetEdmx(DbContext dbContext)
        {
            var ms = new MemoryStream();

            using (XmlWriter writer = XmlWriter.Create(ms))
            {
                EdmxWriter.WriteEdmx(dbContext, writer);
            }

            ms.Position = 0;

            return XDocument.Load(ms);
        }

        private static void SplitEdmx(XDocument edmx, out XmlReader csdlReader, out XmlReader ssdlReader,
                                      out XmlReader mslReader)
        {
            // xml namespace agnostic to make it work with any version of Entity Framework
            Debug.Assert(edmx.Root != null, "edmx.Root != null");
            XNamespace edmxNs = edmx.Root.Name.Namespace;

            XElement storageModels = edmx.Descendants(edmxNs + "StorageModels").Single();
            XElement conceptualModels = edmx.Descendants(edmxNs + "ConceptualModels").Single();
            XElement mappings = edmx.Descendants(edmxNs + "Mappings").Single();

            ssdlReader = storageModels.Elements().Single(e => e.Name.LocalName == "Schema").CreateReader();
            csdlReader = conceptualModels.Elements().Single(e => e.Name.LocalName == "Schema").CreateReader();
            mslReader = mappings.Elements().Single(e => e.Name.LocalName == "Mapping").CreateReader();
        }

        private static void GetItemCollections(XDocument edmx, out EdmItemCollection edmItemCollection,
                                               out StoreItemCollection storeItemCollection,
                                               out StorageMappingItemCollection mappingItemCollection)
        {
            // extract csdl, ssdl and msl artifacts from the Edmx
            XmlReader csdlReader, ssdlReader, mslReader;
            SplitEdmx(edmx, out csdlReader, out ssdlReader, out mslReader);

            // Initialize item collections
            edmItemCollection = new EdmItemCollection(new[] {csdlReader});
            storeItemCollection = new StoreItemCollection(new[] {ssdlReader});
            mappingItemCollection = new StorageMappingItemCollection(edmItemCollection, storeItemCollection,
                                                                     new[] {mslReader});
        }

        private static Dictionary<string, string> GetExtentViews(StorageMappingItemCollection mappingItemCollection,
                                                                 out IList<EdmSchemaError> errors)
        {
            Dictionary<EntitySetBase, string> views = ReflectionHelper.GenerateViews(mappingItemCollection, out errors);
            if (errors != null && errors.Any())
            {
                return null;
            }

            var extentViews = new Dictionary<string, string>(views.Count);

            foreach (var kvp in views)
            {
                extentViews.Add(
                    GetExtentFullName(kvp.Key),
                    kvp.Value.Replace("\r\n", "\n")); // replace accounts for Xml new line normalization
            }

            return extentViews;
        }

        private static string GetExtentFullName(EntitySetBase entitySet)
        {
            return string.Format("{0}.{1}", entitySet.EntityContainer.Name, entitySet.Name);
        }


        private static string CreateViews(Dictionary<string, string> extentViews)
        {
            var sb = new StringBuilder();
            //var embeddedViewsFileName = Path.ChangeExtension(Host.TemplateFile, "xml");
            using (XmlWriter writer = XmlWriter.Create(sb, new XmlWriterSettings
                {
                    Indent = true,
                    Encoding = Encoding.UTF8
                }))
            {
                writer.WriteStartElement("views");
                foreach (var kvp in extentViews)
                {
                    writer.WriteStartElement("view");
                    writer.WriteAttributeString("extent", kvp.Key);
                    writer.WriteCData(kvp.Value);
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }

            return sb.ToString();
        }

        #region Nested type: ReflectionHelper

        private static class ReflectionHelper
        {
            private static readonly Assembly efAssembly = typeof (StorageMappingItemCollection).Assembly;

            private static readonly MethodInfo generateViewsMethodInfo =
                typeof (StorageMappingItemCollection).GetMethod("GenerateEntitySetViews",
                                                                BindingFlags.NonPublic | BindingFlags.Instance);

            private static readonly MethodInfo getMappingClosureHashMethodInfo =
                efAssembly.GetType("System.Data.Entity.Core.Mapping.MetadataMappingHasherVisitor", true)
                          .GetMethod("GetMappingClosureHash", BindingFlags.Static | BindingFlags.NonPublic);

            private static readonly MethodInfo generateHashForAllExtentViewsContentMethodInfo =
                efAssembly.GetType("System.Data.Entity.Core.Common.Utils.MetadataHelper", true)
                          .GetMethod("GenerateHashForAllExtentViewsContent",
                                     BindingFlags.Static | BindingFlags.NonPublic);

            public static Dictionary<EntitySetBase, string> GenerateViews(
                StorageMappingItemCollection mappingItemCollection, out IList<EdmSchemaError> errors)
            {
                errors = null;
                return
                    (Dictionary<EntitySetBase, string>)
                    generateViewsMethodInfo.Invoke(mappingItemCollection, new object[] {errors});
            }

            public static string GetMappingClosureHash(double schemaVersion,
                                                       StorageMappingItemCollection mappingItemCollection)
            {
                return (string) getMappingClosureHashMethodInfo.Invoke(
                    null,
                    new object[]
                        {
                            schemaVersion, 
                            // CodeFirst currently creates always one entity container
                            mappingItemCollection.GetItems<GlobalItem>().Single(
                                i => i.GetType().Name == "StorageEntityContainerMapping")
                        });
            }

            public static string GenerateHashForAllExtentViewsContent(double schemaVersion,
                                                                      Dictionary<string, string> extentViews)
            {
                return (string) generateHashForAllExtentViewsContentMethodInfo.Invoke(
                    null,
                    new object[] {schemaVersion, extentViews});
            }
        }

        #endregion
    }
}